﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SmartMathLibrary
{
    /// <summary>
    /// This class represents a matrix of quaternions.
    /// </summary>
    [Serializable]
    public class QuaternionMatrix : AbstractMatrix, IQuaternionMatrix
    {
        /// <summary>
        /// The number of rows and columns of the matrix.
        /// </summary>
        private readonly int rows;

        /// <summary>
        /// The number of rows and columns of the matrix.
        /// </summary>
        private readonly int columns;

        /// <summary>
        /// The coefficients of the matrix.
        /// </summary>
        private readonly Quaternion[,] matrixData;

        /// <summary>
        /// Initializes a new instance of the <see cref="QuaternionMatrix"/> class.
        /// </summary>
        /// <param name="rows">The number of matrix rows.</param>
        /// <param name="columns">The number of matrix columns.</param>
        public QuaternionMatrix(int rows, int columns)
        {
            if (rows < 0)
            {
                throw new ArgumentException("rows < 0", "rows");
            }

            if (columns < 0)
            {
                throw new ArgumentException("columns < 0", "columns");
            }

            this.columns = columns;
            this.rows = rows;
            this.matrixData = new Quaternion[rows,columns];

            for (int i = 0; i < this.rows; i++)
            {
                for (int j = 0; j < this.columns; j++)
                {
                    this.matrixData[i, j] = new Quaternion();
                }
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="QuaternionMatrix"/> class.
        /// </summary>
        /// <param name="matrix">The data of the matrix in form of a two dimensional array.</param>
        public QuaternionMatrix(Quaternion[,] matrix)
        {
            if (matrix == (Quaternion[,]) ((object) null))
            {
                throw new ArgumentNullException("matrix");
            }

            this.rows = matrix.GetLength(0);
            this.columns = matrix.GetLength(1);
            this.matrixData = new Quaternion[matrix.GetLength(0),matrix.GetLength(1)];

            for (int i = 0; i < matrix.GetLength(0); i++)
            {
                for (int j = 0; j < matrix.GetLength(1); j++)
                {
                    this.matrixData[i, j] = matrix[i, j];
                }
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="QuaternionMatrix"/> class.
        /// </summary>
        /// <param name="matrix">The matrix to clone.</param>
        public QuaternionMatrix(QuaternionMatrix matrix)
        {
            if (matrix == (QuaternionMatrix) null)
            {
                throw new ArgumentNullException("matrix");
            }

            this.rows = matrix.Rows;
            this.columns = matrix.Columns;
            this.matrixData = (Quaternion[,]) matrix.MatrixData.Clone();
        }

        /// <summary>
        /// Gets the number of columns.
        /// </summary>
        public int Columns
        {
            get { return columns; }
        }

        /// <summary>
        /// Gets the number of rows.
        /// </summary>
        public int Rows
        {
            get { return rows; }
        }

        /// <summary>
        /// Gets the matrix data in form of an array.
        /// </summary>
        public Quaternion[,] MatrixData
        {
            get { return matrixData; }
        }

        /// <summary>
        /// Copies the current instance.
        /// </summary>
        /// <returns>A copy of the current instance.</returns>
        public QuaternionMatrix Copy()
        {
            return new QuaternionMatrix((Quaternion[,]) this.matrixData.Clone());
        }

        /// <summary>
        /// Determines whether the current matrix is a square matrix.
        /// </summary>
        /// <value>
        /// True if the current matrix is a square matrix otherwise, false. 
        /// </value>
        public bool IsSquareMatrix
        {
            get { return this.rows == this.columns; }
        }

        /// <summary>
        /// Determines whether the current matrix is a diagonal matrix.
        /// </summary>
        /// <value>
        /// True if the current matrix is a diagonal matrix otherwise, false.
        /// </value>
        public bool IsDiagonalMatrix
        {
            get
            {
                if (!this.IsSquareMatrix)
                {
                    return false;
                }

                for (int i = 0; i < this.rows; i++)
                {
                    for (int j = 0; j < i; j++)
                    {
                        if (i != j)
                        {
                            if ((this.matrixData[i, j].X0 != 0) && (this.matrixData[i, j].I != 0)
                                && (this.matrixData[i, j].J != 0) && (this.matrixData[i, j].K != 0))
                            {
                                return false;
                            }
                        }
                    }
                }

                return true;
            }
        }

        /// <summary>
        /// Determines whether the current matrix is a column vector.
        /// </summary>
        /// <value>
        /// True if the current matrix is a column vector otherwise, false.
        /// </value>
        public bool IsColumnVector
        {
            get { return this.columns == 1; }
        }

        /// <summary>
        /// Determines whether the current matrix is a row vector.
        /// </summary>
        /// <value>
        /// True if this instance is row vector otherwise, false.
        /// </value>
        public bool IsRowVector
        {
            get { return this.rows == 1; }
        }

        /// <summary>
        /// Gets the trace of the matrix.
        /// </summary>
        /// <value>The trace of the matrix.</value>
        public Quaternion Trace
        {
            get
            {
                Quaternion trace = new Quaternion();

                for (int i = 0; i < Math.Min(this.rows, this.columns); i++)
                {
                    trace += this.matrixData[i, i];
                }

                return trace;
            }
        }

        /// <summary>
        /// Extracts the real number part and i number part of the matrix.
        /// </summary>
        /// <returns>A complex matrix which includes the real and i number part of the current matrix.</returns>
        public ComplexMatrix ExtractRealNumberPartAndIMatrix()
        {
            ComplexMatrix result = new ComplexMatrix(this.rows, this.columns);

            for (int i = 0; i < this.rows; i++)
            {
                for (int j = 0; j < this.columns; j++)
                {
                    result.SetValueAtPosition(i, j, new ComplexNumber(this.matrixData[i, j].X0, this.matrixData[i, j].I));
                }
            }

            return result;
        }

        /// <summary>
        /// Extracts the j number part and k number part of the matrix.
        /// </summary>
        /// <returns>A complex matrix which includes the j and k number part of the current matrix.</returns>
        public ComplexMatrix ExtractJAndKMatrix()
        {
            ComplexMatrix result = new ComplexMatrix(this.rows, this.columns);

            for (int i = 0; i < this.rows; i++)
            {
                for (int j = 0; j < this.columns; j++)
                {
                    result.SetValueAtPosition(i, j, new ComplexNumber(this.matrixData[i, j].J, this.matrixData[i, j].K));
                }
            }

            return result;
        }

        /// <summary>
        /// Inverts the sign of all real number parts in the matrix.
        /// </summary>
        public void InvertRealNumberPart()
        {
            for (int i = 0; i < this.rows; i++)
            {
                for (int j = 0; j < this.columns; j++)
                {
                    this.matrixData[i, j].X0 = -this.matrixData[i, j].X0;
                }
            }
        }

        /// <summary>
        /// Inverts the sign of all i number parts in the matrix.
        /// </summary>
        public void InvertINumberPart()
        {
            for (int i = 0; i < this.rows; i++)
            {
                for (int j = 0; j < this.columns; j++)
                {
                    this.matrixData[i, j].I = -this.matrixData[i, j].I;
                }
            }
        }

        /// <summary>
        /// Inverts the sign of all j number parts in the matrix.
        /// </summary>
        public void InvertJNumberPart()
        {
            for (int i = 0; i < this.rows; i++)
            {
                for (int j = 0; j < this.columns; j++)
                {
                    this.matrixData[i, j].J = -this.matrixData[i, j].J;
                }
            }
        }

        /// <summary>
        /// Inverts the sign of all k number parts in the matrix.
        /// </summary>
        public void InvertKNumberPart()
        {
            for (int i = 0; i < this.rows; i++)
            {
                for (int j = 0; j < this.columns; j++)
                {
                    this.matrixData[i, j].K = -this.matrixData[i, j].K;
                }
            }
        }

        /// <summary>
        /// Extracts the real number part of the quaternion values into a new matrix.
        /// </summary>
        /// <returns>The real number part of the quaternion values.</returns>
        public Matrix ExtractRealNumberPartMatrix()
        {
            Matrix result = new Matrix(this.rows, this.columns);

            for (int i = 0; i < this.rows; i++)
            {
                for (int j = 0; j < this.columns; j++)
                {
                    result.SetValueAtPosition(i, j, this.matrixData[i, j].X0);
                }
            }

            return result;
        }

        /// <summary>
        /// Extracts the i number part of the quaternion values into a new matrix.
        /// </summary>
        /// <returns>The i number part of the quaternion values.</returns>
        public Matrix ExtractINumberPartMatrix()
        {
            Matrix result = new Matrix(this.rows, this.columns);

            for (int i = 0; i < this.rows; i++)
            {
                for (int j = 0; j < this.columns; j++)
                {
                    result.SetValueAtPosition(i, j, this.matrixData[i, j].I);
                }
            }

            return result;
        }

        /// <summary>
        /// Extracts the j number part of the quaternion values into a new matrix.
        /// </summary>
        /// <returns>The j number part of the quaternion values.</returns>
        public Matrix ExtractJNumberPartMatrix()
        {
            Matrix result = new Matrix(this.rows, this.columns);

            for (int i = 0; i < this.rows; i++)
            {
                for (int j = 0; j < this.columns; j++)
                {
                    result.SetValueAtPosition(i, j, this.matrixData[i, j].J);
                }
            }

            return result;
        }

        /// <summary>
        /// Extracts the k number part of the quaternion values into a new matrix.
        /// </summary>
        /// <returns>The k number part of the quaternion values.</returns>
        public Matrix ExtractKNumberPartMatrix()
        {
            Matrix result = new Matrix(this.rows, this.columns);

            for (int i = 0; i < this.rows; i++)
            {
                for (int j = 0; j < this.columns; j++)
                {
                    result.SetValueAtPosition(i, j, this.matrixData[i, j].K);
                }
            }

            return result;
        }

        /// <summary>
        /// Calculates the main diagonal product of the current matrix.
        /// </summary>
        /// <returns>The main diagonal product of the current matrix.</returns>
        public Quaternion CalculateMainDiagonalProduct()
        {
            if (this.IsSquareMatrix)
            {
                Quaternion product = new Quaternion(1, 0, 0, 0);

                for (int i = 0; i < this.rows; i++)
                {
                    product *= this.matrixData[i, i];
                }

                return product;
            }

            throw new IllegalArithmeticException("Cannot calculate the main diagonal product of a non square matrix.");
        }

        /// <summary>
        /// Gets the number at a position of the matrix.
        /// </summary>
        /// <param name="position">The position of the value.</param>
        /// <returns>The number at the specified position.</returns>
        public Quaternion GetValueAtPosition(MatrixPosition position)
        {
            if ((position.Row < this.matrixData.GetLength(0)) && (position.Column < this.matrixData.GetLength(1)))
            {
                return this.matrixData[position.Row, position.Column];
            }

            throw new IllegalPositionException("Matrix position does not exist");
        }

        /// <summary>
        /// Gets the value at the specified position of the matrix.
        /// </summary>
        /// <param name="row">The row index where the value should be set.</param>
        /// <param name="column">The column index where the value should be set.</param>
        /// <returns>The value at the specified position.</returns>
        public Quaternion GetValueAtPosition(int row, int column)
        {
            if ((row < this.matrixData.GetLength(0)) && (column < this.matrixData.GetLength(1)))
            {
                return this.matrixData[row, column];
            }

            throw new IllegalPositionException("Matrix position does not exist");
        }

        /// <summary>
        /// Transposes the current matrix.
        /// </summary>
        /// <returns>The transposed matrix.</returns>
        public QuaternionMatrix Transpose()
        {
            QuaternionMatrix result = new QuaternionMatrix(this.columns, this.rows);

            for (int i = 0; i < this.rows; i++)
            {
                for (int j = 0; j < this.columns; j++)
                {
                    result.SetValueAtPosition(new MatrixPosition(j, i),
                                              this.GetValueAtPosition(new MatrixPosition(i, j)));
                }
            }

            return result;
        }

        /// <summary>
        /// Sets the number at a position of the matrix.
        /// </summary>
        /// <param name="position">The position where the value should be set.</param>
        /// <param name="value">The number of the element.</param>
        public void SetValueAtPosition(MatrixPosition position, Quaternion value)
        {
            if ((position.Row < this.matrixData.GetLength(0)) && (position.Column < this.matrixData.GetLength(1)))
            {
                this.matrixData[position.Row, position.Column] = value;
            }
            else
            {
                throw new IllegalPositionException("Matrix position does not exist");
            }
        }

        /// <summary>
        /// Sets the number at a position of the matrix.
        /// </summary>
        /// <param name="row">The row index of the value to set.</param>
        /// <param name="column">The column index of the value to set.</param>
        /// <param name="value">The value to set.</param>
        public void SetValueAtPosition(int row, int column, Quaternion value)
        {
            if ((row < this.matrixData.GetLength(0)) && (column < this.matrixData.GetLength(1)))
            {
                this.matrixData[row, column] = value;
            }
            else
            {
                throw new IllegalPositionException("Matrix position does not exist");
            }
        }

        /// <summary>
        /// Compares the current instance of a matrix to another one.
        /// </summary>
        /// <param name="matrix">The matrix to compare.</param>
        /// <returns>True if both matrices are even otherwise, false.</returns>
        public bool CompareTo(QuaternionMatrix matrix)
        {
            if ((this.columns != matrix.Columns) || (this.rows != matrix.Rows))
            {
                return false;
            }

            for (int i = 0; i < matrix.Rows; i++)
            {
                for (int j = 0; j < matrix.Columns; j++)
                {
                    if (this.GetValueAtPosition(new MatrixPosition(i, j)) !=
                        matrix.GetValueAtPosition(new MatrixPosition(i, j)))
                    {
                        return false;
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="a">The first matrix.</param>
        /// <param name="b">The second matrix.</param>
        /// <returns>The result of the operator.</returns>
        public static QuaternionMatrix operator *(QuaternionMatrix a, Quaternion b)
        {
            if (a == (QuaternionMatrix) null)
            {
                throw new ArgumentNullException("a");
            }

            if (b == (Quaternion) null)
            {
                throw new ArgumentNullException("b");
            }

            QuaternionMatrix result = a;

            for (int i = 0; i < result.Rows; i++)
            {
                for (int j = 0; j < result.Columns; j++)
                {
                    result.SetValueAtPosition(new MatrixPosition(i, j),
                                              result.GetValueAtPosition(new MatrixPosition(i, j)) * b);
                }
            }

            return result;
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="a">The first matrix.</param>
        /// <param name="b">The second matrix.</param>
        /// <returns>The result of the operator.</returns>
        public static QuaternionMatrix operator *(QuaternionMatrix a, QuaternionMatrix b)
        {
            if (a == (QuaternionMatrix) null)
            {
                throw new ArgumentNullException("a");
            }

            if (b == (QuaternionMatrix) null)
            {
                throw new ArgumentNullException("b");
            }

            if (a.Columns == b.Rows)
            {
                QuaternionMatrix result = new QuaternionMatrix(a.Rows, b.Columns);
                Quaternion[,] resultData = result.MatrixData;
                Quaternion[] bcolj = new Quaternion[a.Columns];

                for (int j = 0; j < b.Columns; j++)
                {
                    for (int k = 0; k < a.Columns; k++)
                    {
                        bcolj[k] = b.MatrixData[k, j];
                    }

                    for (int i = 0; i < a.Rows; i++)
                    {
                        Quaternion[] arowi = new Quaternion[a.Columns];

                        for (int l = 0; l < a.Columns; l++)
                        {
                            arowi[l] = a.MatrixData[i, l];
                        }

                        Quaternion s = new Quaternion();

                        for (int k = 0; k < a.Columns; k++)
                        {
                            s += arowi[k] * bcolj[k];
                        }

                        resultData[i, j] = s;
                    }
                }

                return result;
            }

            throw new IllegalArithmeticException(
                "The rows of the one side operator and the columns of the other side operator have to be even");
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="matrix">The matrix.</param>
        /// <param name="vector">The vector for the multiplication.</param>
        /// <returns>The result of the operator.</returns>
        public static GeneralQuaternionVector operator *(QuaternionMatrix matrix, GeneralQuaternionVector vector)
        {
            if (vector == (GeneralQuaternionVector) null)
            {
                throw new ArgumentNullException("vector");
            }

            if (matrix == (QuaternionMatrix) null)
            {
                throw new ArgumentNullException("matrix");
            }

            if (vector.Count != matrix.Columns)
            {
                throw new ArithmeticException(
                    "The number of vector rows and the number of matrix columns have to be even.");
            }

            GeneralQuaternionVector result = new GeneralQuaternionVector(matrix.Rows);

            for (int i = 0; i < matrix.Rows; i++)
            {
                Quaternion tempuri = new Quaternion();

                for (int j = 0; j < matrix.Columns; j++)
                {
                    tempuri += matrix.GetValueAtPosition(i, j) * vector[j];
                }

                result[i] = tempuri;
            }

            return result;
        }

        /// <summary>
        /// Implements the operator +.
        /// </summary>
        /// <param name="a">The first matrix.</param>
        /// <param name="b">The second matrix.</param>
        /// <returns>The result of the operator.</returns>
        public static QuaternionMatrix operator +(QuaternionMatrix a, QuaternionMatrix b)
        {
            if (a == (QuaternionMatrix) null)
            {
                throw new ArgumentNullException("a");
            }

            if (b == (QuaternionMatrix) null)
            {
                throw new ArgumentNullException("b");
            }

            if ((a.Rows == b.Rows) && (a.Columns == b.Columns))
            {
                QuaternionMatrix result = a;

                for (int i = 0; i < result.Rows; i++)
                {
                    for (int j = 0; j < result.Columns; j++)
                    {
                        result.SetValueAtPosition(new MatrixPosition(i, j),
                                                  result.GetValueAtPosition(new MatrixPosition(i, j))
                                                  + b.GetValueAtPosition(new MatrixPosition(i, j)));
                    }
                }

                return result;
            }

            throw new IllegalArithmeticException("The matrix dimension has to be even");
        }

        /// <summary>
        /// Implements the operator -.
        /// </summary>
        /// <param name="a">The first matrix.</param>
        /// <param name="b">The second matrix.</param>
        /// <returns>The result of the operator.</returns>
        public static QuaternionMatrix operator -(QuaternionMatrix a, QuaternionMatrix b)
        {
            if (a == (QuaternionMatrix) null)
            {
                throw new ArgumentNullException("a");
            }

            if (b == (QuaternionMatrix) null)
            {
                throw new ArgumentNullException("b");
            }

            if ((a.Rows == b.Rows) && (a.Columns == b.Columns))
            {
                QuaternionMatrix result = a;

                for (int i = 0; i < result.Rows; i++)
                {
                    for (int j = 0; j < result.Columns; j++)
                    {
                        result.SetValueAtPosition(new MatrixPosition(i, j),
                                                  result.GetValueAtPosition(new MatrixPosition(i, j))
                                                  - b.GetValueAtPosition(new MatrixPosition(i, j)));
                    }
                }

                return result;
            }

            throw new IllegalArithmeticException("The matrix dimension has to be even");
        }

        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="a">The first matrix.</param>
        /// <param name="b">The second matrix.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator ==(QuaternionMatrix a, QuaternionMatrix b)
        {
            if (System.Object.ReferenceEquals(a, b))
            {
                return true;
            }

            if (((object) a == null) || ((object) b == null))
            {
                return false;
            }

            return a.CompareTo(b);
        }

        /// <summary>
        /// Implements the operator !=.
        /// </summary>
        /// <param name="a">The first matrix.</param>
        /// <param name="b">The second matrix.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator !=(QuaternionMatrix a, QuaternionMatrix b)
        {
            return !(a == b);
        }

        /// <summary>
        /// Unions the specified matrices. The two matrices have to have the same number of
        /// rows.
        /// </summary>
        /// <param name="a">The first matrix.</param>
        /// <param name="b">The second matrix.</param>
        /// <returns>The union matrix.</returns>
        public static QuaternionMatrix UnionMatricesVertical(QuaternionMatrix a, QuaternionMatrix b)
        {
            if (a.Rows != b.Rows)
            {
                throw new ArgumentException("The number of rows in both matrices has to be even.");
            }

            QuaternionMatrix result = new QuaternionMatrix(a.Rows, a.Columns + b.Columns);

            for (int i = 0; i < a.Rows; i++)
            {
                for (int j = 0; j < a.Columns; j++)
                {
                    result.SetValueAtPosition(new MatrixPosition(i, j),
                                              a.GetValueAtPosition(new MatrixPosition(i, j)).Copy());
                }

                for (int j = 0; j < b.Columns; j++)
                {
                    result.SetValueAtPosition(new MatrixPosition(i, j + a.Columns),
                                              b.GetValueAtPosition(new MatrixPosition(i, j)).Copy());
                }
            }

            return result;
        }

        /// <summary>
        /// Unions the specified matrices. The two matrices have to have the same number of
        /// columns.
        /// </summary>
        /// <param name="a">The first matrix.</param>
        /// <param name="b">The second matrix.</param>
        /// <returns>The union matrix.</returns>
        public static QuaternionMatrix UnionMatricesHorizontal(QuaternionMatrix a, QuaternionMatrix b)
        {
            if (a.Columns != b.Columns)
            {
                throw new ArgumentException("The number of columns have to be even in both matricies.");
            }

            QuaternionMatrix result = new QuaternionMatrix(a.Rows + b.Rows, a.Columns);

            for (int i = 0; i < a.Rows; i++)
            {
                for (int j = 0; j < a.Columns; j++)
                {
                    result.MatrixData[i, j] = a.MatrixData[i, j].Copy();
                }
            }

            int offset = a.Rows;

            for (int i = 0; i < b.Rows; i++)
            {
                for (int j = 0; j < b.Columns; j++)
                {
                    result.MatrixData[i + offset, j] = b.MatrixData[i, j].Copy();
                }
            }

            return result;
        }

        /// <summary>
        /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
        /// </summary>
        /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
        /// <returns>
        /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
        /// </returns>
        /// <exception cref="T:System.NullReferenceException">The <paramref name="obj"/> parameter is null.</exception>
        public override bool Equals(object obj)
        {
            return base.Equals(obj);
        }

        /// <summary>
        /// Serves as a hash function for a particular type.
        /// </summary>
        /// <returns>
        /// A hash code for the current <see cref="T:System.Object"/>.
        /// </returns>
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        /// <summary>
        /// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
        /// </returns>
        public override String ToString()
        {
            String result = "";

            for (int i = 0; i < this.rows; i++)
            {
                for (int j = 0; j < this.columns; j++)
                {
                    result += this.matrixData[i, j].ToString() + "\t";
                }

                result += "\n";
            }

            return result;
        }
    }
}